// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

/**
 * Class to manipulate the window in the remote extension.
 *
 * @param {string} extensionId ID of extension to be manipulated.
 * @constructor
 */
function RemoteCall(extensionId) {
  this.extensionId_ = extensionId;
}

/**
 * Checks whether step by step tests are enabled or not.
 * @return {Promise<bool>}
 */
RemoteCall.isStepByStepEnabled = function() {
  return new Promise(function(fulfill) {
    chrome.commandLinePrivate.hasSwitch(
        'enable-file-manager-step-by-step-tests', fulfill);
  });
};

/**
 * Calls a remote test util in Files.app's extension. See: test_util.js.
 *
 * @param {string} func Function name.
 * @param {?string} appId Target window's App ID or null for functions
 *     not requiring a window.
 * @param {Array<*>} args Array of arguments.
 * @param {function(*)=} opt_callback Callback handling the function's result.
 * @return {Promise} Promise to be fulfilled with the result of the remote
 *     utility.
 */
RemoteCall.prototype.callRemoteTestUtil =
    function(func, appId, args, opt_callback) {
  return RemoteCall.isStepByStepEnabled().then(function(stepByStep) {
    if (!stepByStep)
      return false;
    return new Promise(function(onFulfilled) {
      console.info('Executing: ' + func + ' on ' + appId + ' with args: ');
      console.info(args);
      console.info('Type step() to continue...');
      window.step = function() {
        window.step = null;
        onFulfilled(stepByStep);
      };
    });
  }).then(function(stepByStep) {
    return new Promise(function(onFulfilled) {
      chrome.runtime.sendMessage(
          this.extensionId_,
          {
            func: func,
            appId: appId,
            args: args
          },
          function(var_args) {
            if (stepByStep) {
              console.info('Returned value:');
              console.info(arguments);
            }
            if (opt_callback)
              opt_callback.apply(null, arguments);
            onFulfilled(arguments[0]);
          });
    }.bind(this));
  }.bind(this));
};

/**
 * Waits until a window having the given ID prefix appears.
 * @param {string} windowIdPrefix ID prefix of the requested window.
 * @return {Promise} promise Promise to be fulfilled with a found window's ID.
 */
RemoteCall.prototype.waitForWindow = function(windowIdPrefix) {
  return repeatUntil(function() {
    return this.callRemoteTestUtil('getWindows', null, []).
        then(function(windows) {
      for (var id in windows) {
        if (id.indexOf(windowIdPrefix) === 0)
          return id;
      }
      return pending('Window with the prefix %s is not found.', windowIdPrefix);
    });
  }.bind(this));
};

/**
 * Closes a window and waits until the window is closed.
 *
 * @param {string} windowId ID of the window to close.
 * @return {Promise} promise Promise to be fulfilled with the result (true:
 *     success, false: failed).
 */
RemoteCall.prototype.closeWindowAndWait = function(windowId) {
  // Closes the window.
  return this.callRemoteTestUtil('closeWindow', null, [windowId]).then(
      function(result) {
        // Returns false when the closing is failed.
        if (!result)
          return false;

        return repeatUntil(function() {
          return this.callRemoteTestUtil('getWindows', null, []).then(
              function(windows) {
                for (var id in windows) {
                  if (id === windowId) {
                    // Window is still available. Continues waiting.
                    return pending('Window with the prefix %s is not found.',
                                   windowId);
                  }
                }
                // Window is not available. Closing is done successfully.
                return true;
              }
          );
        }.bind(this));
      }.bind(this)
  );
};

/**
 * Waits until the window turns to the given size.
 * @param {string} windowId Target window ID.
 * @param {number} width Requested width in pixels.
 * @param {number} height Requested height in pixels.
 */
RemoteCall.prototype.waitForWindowGeometry =
    function(windowId, width, height) {
  return repeatUntil(function() {
    return this.callRemoteTestUtil('getWindows', null, []).
        then(function(windows) {
      if (!windows[windowId])
        return pending('Window %s is not found.', windowId);
      if (windows[windowId].outerWidth !== width ||
          windows[windowId].outerHeight !== height) {
        return pending('Expected window size is %j, but it is %j',
                       {width: width, height: height},
                       windows[windowId]);
      }
    });
  }.bind(this));
};

/**
 * Waits for the specified element appearing in the DOM.
 * @param {string} windowId Target window ID.
 * @param {string} query Query string for the element.
 * @param {string=} opt_iframeQuery Query string for the iframe containing the
 *     element.
 * @return {Promise} Promise to be fulfilled when the element appears.
 */
RemoteCall.prototype.waitForElement =
    function(windowId, query, opt_iframeQuery) {
  return repeatUntil(function() {
    return this.callRemoteTestUtil(
        'queryAllElements',
        windowId,
        [query, opt_iframeQuery]
    ).then(function(elements) {
      if (elements.length > 0)
        return elements[0];
      else
        return pending(
            'Element %s (maybe in iframe %s) is not found.',
            query,
            opt_iframeQuery);
    });
  }.bind(this));
};

/**
 * Waits for the specified element leaving from the DOM.
 * @param {string} windowId Target window ID.
 * @param {string} query Query string for the element.
 * @param {string=} opt_iframeQuery Query string for the iframe containing the
 *     element.
 * @return {Promise} Promise to be fulfilled when the element is lost.
 */
RemoteCall.prototype.waitForElementLost =
    function(windowId, query, opt_iframeQuery) {
  return repeatUntil(function() {
    return this.callRemoteTestUtil(
        'queryAllElements',
        windowId,
        [query, opt_iframeQuery]
    ).then(function(elements) {
      if (elements.length > 0)
        return pending('Elements %j is still exists.', elements);
      return true;
    });
  }.bind(this));
};

/**
 * Sends a fake key down event.
 * @param {string} windowId Window ID.
 * @param {string} query Query for the target element.
 * @param {string} keyIdentifer Key identifier.
 * @param {boolean} ctrlKey Control key flag.
 * @param {boolean} shiftKey Shift key flag.
 * @return {Promise} Promise to be fulfilled or rejected depending on the
 *     result.
 */
RemoteCall.prototype.fakeKeyDown =
    function(windowId, query, keyIdentifer, ctrlKey, shiftKey) {
  var resultPromise = this.callRemoteTestUtil(
      'fakeKeyDown', windowId, [query, keyIdentifer, ctrlKey, shiftKey]);
  return resultPromise.then(function(result) {
    if (result)
      return true;
    else
      return Promise.reject('Fail to fake key down.');
  });
};

/**
 * Gets file entries just under the volume.
 *
 * @param {VolumeManagerCommon.VolumeType} volumeType Volume type.
 * @param {Array<string>} names File name list.
 * @return {Promise} Promise to be fulfilled with file entries or rejected
 *     depending on the result.
 */
RemoteCall.prototype.getFilesUnderVolume = function(volumeType, names) {
  return this.callRemoteTestUtil(
      'getFilesUnderVolume', null, [volumeType, names]);
};

/**
 * Waits for a single file.
 * @param {VolumeManagerCommon.VolumeType} volumeType Volume type.
 * @param {string} name File name.
 * @return {!Promise} Promise to be fulfilled when the file had found.
 */
RemoteCall.prototype.waitForAFile = function(volumeType, name) {
  return repeatUntil(function() {
    return this.getFilesUnderVolume(volumeType, [name])
        .then(function(urls) {
          if (urls.length === 1)
            return true;
          return pending('"' + name + '" is not found.');
        });
  }.bind(this));
};

/**
 * Class to manipulate the window in the remote extension.
 *
 * @param {string} extensionId ID of extension to be manipulated.
 * @extends {RemoteCall}
 * @constructor
 */
function RemoteCallFilesApp() {
  RemoteCall.apply(this, arguments);
}

RemoteCallFilesApp.prototype.__proto__ = RemoteCall.prototype;

/**
 * Waits for the file list turns to the given contents.
 * @param {string} windowId Target window ID.
 * @param {Array<Array<string>>} expected Expected contents of file list.
 * @param {{orderCheck:boolean=, ignoreLastModifiedTime:boolean=}=} opt_options
 *     Options of the comparison. If orderCheck is true, it also compares the
 *     order of files. If ignoreLastModifiedTime is true, it compares the file
 *     without its last modified time.
 * @return {Promise} Promise to be fulfilled when the file list turns to the
 *     given contents.
 */
RemoteCallFilesApp.prototype.waitForFiles =
    function(windowId, expected, opt_options) {
  var options = opt_options || {};
  return repeatUntil(function() {
    return this.callRemoteTestUtil(
        'getFileList', windowId, []).then(function(files) {
      if (!options.orderCheck) {
        files.sort();
        expected.sort();
      }
      for (var i = 0; i < Math.min(files.length, expected.length); i++) {
        if (options.ignoreFileSize) {
          files[i][1] = '';
          expected[i][1] = '';
        }
        if (options.ignoreLastModifiedTime) {
          files[i][3] = '';
          expected[i][3] = '';
        }
      }
      if (!chrome.test.checkDeepEq(expected, files)) {
        return pending('waitForFiles: expected: %j actual %j.',
                       expected,
                       files);
      }
    });
  }.bind(this));
};

/**
 * Waits until the number of files in the file list is changed from the given
 * number.
 * TODO(hirono): Remove the function.
 *
 * @param {string} windowId Target window ID.
 * @param {number} lengthBefore Number of items visible before.
 * @return {Promise} Promise to be fulfilled with the contents of files.
 */
RemoteCallFilesApp.prototype.waitForFileListChange =
    function(windowId, lengthBefore) {
  return repeatUntil(function() {
    return this.callRemoteTestUtil(
        'getFileList', windowId, []).then(function(files) {
      files.sort();
      var notReadyRows = files.filter(function(row) {
        return row.filter(function(cell) { return cell == '...'; }).length;
      });
      if (notReadyRows.length === 0 &&
          files.length !== lengthBefore &&
          files.length !== 0) {
        return files;
      } else {
        return pending('The number of file is %d. Not changed.', lengthBefore);
      }
    });
  }.bind(this));
};

/**
 * Waits until the given taskId appears in the executed task list.
 * @param {string} windowId Target window ID.
 * @param {string} taskId Task ID to watch.
 * @return {Promise} Promise to be fulfilled when the task appears in the
 *     executed task list.
 */
RemoteCallFilesApp.prototype.waitUntilTaskExecutes =
    function(windowId, taskId) {
  return repeatUntil(function() {
    return this.callRemoteTestUtil('getExecutedTasks', windowId, []).
        then(function(executedTasks) {
          if (executedTasks.indexOf(taskId) === -1)
            return pending('Executed task is %j', executedTasks);
        });
  }.bind(this));
};

/**
 * Check if the next tabforcus'd element has the given ID or not.
 * @param {string} windowId Target window ID.
 * @param {string} elementId String of 'id' attribute which the next tabfocus'd
 *     element should have.
 * @return {Promise} Promise to be fulfilled with the result.
 */
RemoteCallFilesApp.prototype.checkNextTabFocus =
    function(windowId, elementId) {
  return remoteCall.callRemoteTestUtil('fakeKeyDown',
                                       windowId,
                                       ['body', 'U+0009', false]).then(
  function(result) {
    chrome.test.assertTrue(result);
    return remoteCall.callRemoteTestUtil('getActiveElement',
                                         windowId,
                                         []);
  }).then(function(element) {
    if (!element || !element.attributes['id'])
      return false;

    if (element.attributes['id'] === elementId) {
      return true;
    } else {
      console.error('The ID of the element should be "' + elementId +
                    '", but "' + element.attributes['id'] + '"');
      return false;
    }
  });
};

/**
 * Waits until the current directory is changed.
 * @param {string} windowId Target window ID.
 * @param {string} expectedPath Path to be changed to.
 * @return {Promise} Promise to be fulfilled when the current directory is
 *     changed to expectedPath.
 */
RemoteCallFilesApp.prototype.waitUntilCurrentDirectoryIsChanged =
    function(windowId, expectedPath) {
  return repeatUntil(function () {
    return this.callRemoteTestUtil('getBreadcrumbPath', windowId, []).then(
      function(path) {
        if(path !== expectedPath)
          return pending('Expected path is %s', expectedPath);
      });
  }.bind(this));
};

/**
 * Class to manipulate the window in the remote extension.
 *
 * @param {string} extensionId ID of extension to be manipulated.
 * @extends {RemoteCall}
 * @constructor
 */
function RemoteCallGallery() {
  RemoteCall.apply(this, arguments);
}

RemoteCallGallery.prototype.__proto__ = RemoteCall.prototype;

/**
 * Waits until the expected image is shown.
 *
 * @param {document} document Document.
 * @param {number} width Expected width of the image.
 * @param {number} height Expected height of the image.
 * @param {string|null} name Expected name of the image.
 * @return {Promise} Promsie to be fulfilled when the check is passed.
 */
RemoteCallGallery.prototype.waitForSlideImage =
    function(windowId, width, height, name) {
  var expected = {};
  if (width)
    expected.width = width;
  if (height)
    expected.height = height;
  if (name)
    expected.name = name;

  return repeatUntil(function() {
    var query = '.gallery[mode="slide"] .image-container > .image';
    return Promise.all([
        this.waitForElement(windowId, '.filename-spacer input'),
        this.waitForElement(windowId, query)
    ]).then(function(args) {
      var nameBox = args[0];
      var image = args[1];
      var actual = {};
      if (width && image)
        actual.width = image.imageWidth;
      if (height && image)
        actual.height = image.imageHeight;
      if (name && nameBox)
        actual.name = nameBox.value;

      if (!chrome.test.checkDeepEq(expected, actual)) {
        return pending('Slide mode state, expected is %j, actual is %j.',
                       expected, actual);
      }
      return actual;
    });
  }.bind(this));
};

RemoteCallGallery.prototype.changeNameAndWait = function(windowId, newName) {
  return this.callRemoteTestUtil('changeName', windowId, [newName]
  ).then(function() {
    return this.waitForSlideImage(windowId, 0, 0, newName);
  }.bind(this));
};

/**
 * Shorthand for clicking an element.
 * @param {AppWindow} appWindow Application window.
 * @param {string} query Query for the element.
 * @param {Promise} Promise to be fulfilled with the clicked element.
 */
RemoteCallGallery.prototype.waitAndClickElement = function(windowId, query) {
  return this.waitForElement(windowId, query).then(function(element) {
    return this.callRemoteTestUtil('fakeMouseClick', windowId, [query])
    .then(function() { return element; });
  }.bind(this));
};

/**
 * Waits for the "Press Enter" message.
 *
 * @param {AppWindow} appWindow App window.
 * @return {Promise} Promise to be fulfilled when the element appears.
 */
RemoteCallGallery.prototype.waitForPressEnterMessage = function(appId) {
  return this.waitForElement(appId, '.prompt-wrapper .prompt').
      then(function(element) {
        chrome.test.assertEq(
            'Press Enter when done', element.text.trim());
      });
};

/**
 * Shorthand for selecting an image in thumbnail mode.
 * @param {string} appId App id.
 * @param {string} name File name to be selected.
 * @return {!Promise<boolean>} A promise which will be resolved with true if the
 *     thumbnail has clicked. This method does not guarantee whether the
 *     thumbnail has actually selected or not.
 */
RemoteCallGallery.prototype.selectImageInThumbnailMode = function(appId, name) {
  return this.callRemoteTestUtil('fakeMouseClick', appId,
      ['.thumbnail-view > ul > li[title="' + name + '"] > .selection.frame']);
};
